---
title: 概念
---

import SvgStreams from './streams.svg';
import SvgStreamInput from './stream-input.svg';
import SvgStreamOutput from './stream-output.svg';
import SvgStreamTransform from './stream-transform.svg';
import SvgPipelineFilters from './pipeline-filters.svg';
import SvgSubPipeline from './sub-pipeline.svg';
import SvgModulePipelineContext from './module-pipeline-context.svg';

## 流

Pipy 是一个代理，它的工作方式像一个 *“流处理器”*：吞入*流*、处理*流*、吐出*流*。

### 入站和出站

来自客户端（或者下游）的流称为*入站流*；发往服务器（或者上游）的流称为*出站流*。

<div style="text-align: center">
  <SvgStreams/>
</div>

无论入站侧还是出站侧，都既有输入的流也有输出的流：

* 在入站一侧，输入流从客户端流向 Pipy，而输出流则从 Pipy 返回客户端。
* 在出站一侧，输出流从 Pipy 流向服务器，而输入流从服务器返回 Pipy。

### 事件

一个 Pipy 流是一系列的**事件**。事件类型有四种：

* Data
* MessageStart
* MessageEnd
* StreamEnd

来自 Pipy 外部的输入流由一系列 **Data** 事件组成，并由 **StreamEnd** 事件结束。每个 **Data** 事件持有一小块来自 I/O 的字节数据。

例如，一个典型的 HTTP 请求类似这样：

<div style="text-align: center">
  <SvgStreamInput/>
</div>

Pipy 要做的是，处理输入流中的事件：有些会被转换，有些会被丢弃，也有些新的事件会加入。这些新生成的事件也包括除 **Data** 和 **StreamEnd** 之外的其他类型： 即 **MessageStart** 和 **MessageEnd**。这些非数据事件被用作 _“标记”_，为承载业务逻辑的原始字节流提供更高级的语义。

例如，上面的输入流会被解码成一个 _HTTP 请求消息_，并以一对 **MessageStart** 和 **MessageEnd** 事件包围。这个消息接着会被变换成一个 _HTTP 响应消息_，然后被编码成一系列 **Data** 事件，最后去往输出。

<div style="text-align: center">
  <SvgStreamTransform/>
</div>

最终，经过所有的处理，事件流被发送到输出。此时，**MessageStart** 和 **MessageEnd** 事件被丢弃，只有 **Data** 和 **StreamEnd** 事件被吐出来。

<div style="text-align: center">
  <SvgStreamOutput/>
</div>

## 过滤器和管道

理解管道（Pipeline）的直观办法是将其想象成 [_Unix 管道_](https://en.wikipedia.org/wiki/Pipeline_(Unix))。唯一与 Unix 管道不同的是 Pipy 处理的是事件流而不是字节流。

Pipy 内部通过一串**过滤器（Filter）**来处理进来的流。每个过滤器有点像一个小型 Unix 进程，从标准输入（stdin）读、往标准输出（stdout）写，上一个过滤器的输出就是下一个过滤器的输入。

<div style="text-align: center">
  <SvgPipelineFilters/>
</div>

过滤器连成一串就叫作 **管道**。按其输入源的类型，管道分成四类：

### 端口管道

**端口管道（port pipeline）** 会在某个监听端口上有新的 TCP 连接（或者 UDP 虚拟会话）接入时被创建。它从网络端口读取并处理 **Data** 事件，然后将结果写回客户端。这就类似于广泛采用的 HTTP *_请求响应_* 通信模型，管道的输入是请求，输出是响应。每个进来的连接都有一个专属的_端口管道_，用于处理该连接上的双向通信。

### 定时器管道

**定时器管道（timer pipeline）** 可以被周期性地创建。该管道在创建时能够产生任意所需的输入，经过其内部所有过滤器的处理后，一切输出仅被简单丢弃。该类型的管道可以被用来执行类似 [_cron job_](https://en.wikipedia.org/wiki/Cron) 的任务。

### 信号管道

当 Pipy 进程接收到信号时，一个 **信号管道（signal pipeline）** 会被创建。在创建时，信号管道能够产生任意所需的输入，经过其内部所有过滤器的处理后，一切输出仅被简单丢弃。该类型的管道可用于要在某些信号发生时执行的任务。

### 子管道

**子管道**是可以通过**接合过滤器（joint filter）**从其他管道启动的管道。最基本的接合过滤器是 [link()](/reference/api/Configuration/link)。该过滤器从它所在管道中的上一个过滤器接收事件，将它们输送到子管道进行处理，并回读该子管道的全部输出，然后输送给下一个过滤器。

<div style="text-align: center">
  <SvgSubPipeline/>
</div>

*子管道*和*接合过滤器*就像在过程式编程里调用一个子例程时的*被调用方*和*调用方*。接合过滤器的输入是子例程的参数，接合过滤器的输出是它的返回值。

与子管道不同，其他类型的管道——**端口管道**、**文件管道**、**定时器管道** 和 **信号管道**——不能被接合过滤器从内部 _“调用”_，它们只能从外部源来启动。我们称这些管道为**根管道（root pipelines）**。

## 模块

**模块** 是一个 PipyJS 源文件，里面的脚本定义了一系列**管道布局**。

**管道布局**规定了管道包含哪些过滤器及其顺序。在模块中配置管道布局并不会立即创建管道。此时它只是定义了管道的样子，只有在运行中处理输入的时候才会创建管道。但有些情况下，当含义很明确时，为了简洁，我们使用术语 _“管道”_ 来指代 _“管道布局”_。

## 上下文

**上下文（Context）** 是归属于管道的一套变量，脚本利用它来维护一个管道的当前状态。

一个 Pipy 模块里的所有管道都使用同样一套变量。换句话说，一个模块里的所有管道，它们的上下文具有相同的 *“形态”*，而不同模块的上下文则可以具有不同形态。在实现一个 Pipy 模块时，要做的第一件事就是通过调用内置函数 [pipy()](/reference/api/pipy) 来定义模块中使用的上下文的*形态*，也就是它有哪些变量，以及变量的初始值。

虽然一个模块里的所有管道具有完全相同的一套上下文变量，但每个管道可以拥有与其他管道隔离的变量 _“值”_，或者说，每个管道都有其自己的 _“状态”_。

<div style="text-align: center">
  <SvgModulePipelineContext/>
</div>

对于模块中的脚本来说，上下文变量就像**全局变量**一样，从同一个模块文件里的任何位置都可以通过脚本访问这些变量。然而，它们 **不是** 我们通常所理解的那种 _全局变量_。

_“全局变量”_ 通常意味着 _“全局唯一”_，在某个时刻，这些变量应该只有一个状态，而在 Pipy 中，根据当前不同上下文的数量，这些变量可以有多个不同状态。如果熟悉多线程编程的概念，可以把**上下文**理解成 [*TLS（线程局部存储）*](https://en.wikipedia.org/wiki/Thread-local_storage)，不同的*线程*可以有处于不同状态的*线程局部变量*，或者就 Pipy 来说，*上下文变量*在不同*管道*中的状态可以不同。
